package com.itaming.lycheeframework.security.token;

import com.itaming.lycheeframework.security.constant.TokenConstant;
import com.itaming.lycheeframework.security.exception.AuthenticationException;
import com.itaming.lycheeframework.security.properties.LycheeSecurityProperties;
import com.itaming.lycheeframework.security.result.SecurityErrorCode;
import com.itaming.lycheeframework.security.token.cache.TokenCache;
import com.itaming.lycheeframework.security.token.cache.TokenCacheService;
import com.itaming.lycheeframework.security.token.provider.TokenExpiresInProvider;
import com.itaming.lycheeframework.security.token.provider.TokenProvider;
import com.itaming.lycheeframework.support.utils.Kv;
import com.itaming.lycheeframework.support.utils.ObjectUtil;
import com.itaming.lycheeframework.support.utils.StringUtil;
import com.itaming.lycheeframework.support.utils.URLUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;

/**
 * Token服务
 *
 * @author A.Ming
 */
@RequiredArgsConstructor
@Slf4j
public class TokenService {

    /**
     * 载荷key：标识符
     */
    public static final String PAYLOAD_KEY_ID = "_id";

    /**
     * 载荷key：客户端标识符
     */
    public static final String PAYLOAD_KEY_CLIENT = "_client";

    /**
     * 载荷key：类型
     */
    public static final String PAYLOAD_KEY_TYPE = "_type";

    /**
     * Token提供者
     */
    private final TokenProvider tokenProvider;

    /**
     * Token过期时间提供者
     */
    private final TokenExpiresInProvider tokenExpiresInProvider;

    /**
     * Token缓存服务
     */
    private final TokenCacheService tokenCacheService;

    /**
     * Token配置参数
     */
    private final LycheeSecurityProperties.Token tokenProperties;

    /**
     * 创建用户Token
     *
     * @param userId   用户标识符
     * @param clientId 客户端标识符
     * @return TokenResponse
     */
    public TokenResponse createUserToken(Object userId, String clientId) {
        return createUserToken(userId, clientId, null);
    }

    /**
     * 创建用户Token
     *
     * @param userId   用户标识符
     * @param clientId 客户端标识符
     * @param userInfo 用户信息
     * @return TokenResponse
     */
    public TokenResponse createUserToken(Object userId, String clientId, Object userInfo) {
        // 检查标识符
        checkId(userId);

        // 检查客户端标识符
        checkClientId(clientId);

        // 载荷
        Map<String, Object> accessTokenPayload = assembleTokenPayload(userId, clientId, TokenType.ACCESS);
        Map<String, Object> refreshTokenPayload = assembleTokenPayload(userId, clientId, TokenType.REFRESH);

        // 获取Token过期时间
        TokenExpiresInPair tokenExpiresInPair = tokenExpiresInProvider.get(clientId);

        // 生成Token
        String accessToken = tokenProvider.createToken(accessTokenPayload, tokenExpiresInPair.getAccess());
        String refreshToken = tokenProvider.createToken(refreshTokenPayload, tokenExpiresInPair.getRefresh());

        // 有状态，保存到缓存
        if (tokenProperties.isStateful()) {
            TokenCache tokenCache = TokenCache.builder()
                .accessToken(accessToken)
                .accessTokenExpiresIn(tokenExpiresInPair.getAccess())
                .refreshToken(refreshToken)
                .refreshTokenExpiresIn(tokenExpiresInPair.getRefresh())
                .id(userId)
                .clientId(clientId)
                .build();
            getTokenCacheService().save(tokenCache);
        }

        // 返回Token
        return TokenResponse.builder()
            .accessToken(accessToken)
            .expiresIn(tokenExpiresInPair.getAccess())
            .refreshToken(refreshToken)
            .userInfo(userInfo)
            .build();
    }

    /**
     * 刷新用户Token
     *
     * @param refreshToken refreshToken
     * @return TokenResponse
     */
    public TokenResponse refreshUserToken(String refreshToken) throws AuthenticationException {
        // 校验是否为RefreshToken
        Map<String, Object> payload = parseToken(refreshToken);
        checkTokenType(payload, TokenType.REFRESH);

        // 解析标识符和客户端标识符
        Object id = parseId(payload);
        String clientId = parseClientId(payload);

        // 有状态，删除旧token
        if (tokenProperties.isStateful()) {
            getTokenCacheService().remove(id, clientId, TokenType.REFRESH, refreshToken);
        }

        // 生成并返回新的Token
        return createUserToken(id, clientId);
    }

    /**
     * 获取标识符
     *
     * @param request 请求
     * @return 标识符
     */
    public <T> T getId(HttpServletRequest request) throws AuthenticationException {
        return getId(extractToken(request));
    }

    /**
     * 获取标识符
     *
     * @param token token
     * @return 标识符
     */
    public <T> T getId(String token) throws AuthenticationException {
        // 直接解析传入的token
        return parseId(parseToken(token));
    }

    /**
     * 验证Token
     *
     * @param request 请求
     * @throws AuthenticationException 验证失败时抛出
     */
    public void verifyToken(HttpServletRequest request) throws AuthenticationException {
        verifyToken(extractToken(request));
    }

    /**
     * 验证Token
     *
     * @param token token
     * @throws AuthenticationException 验证失败时抛出
     */
    public void verifyToken(String token) throws AuthenticationException {
        Map<String, Object> payload = parseToken(token);
        checkTokenType(payload, TokenType.ACCESS);
    }

    /**
     * 移除Token
     *
     * @param request 请求
     * @return boolean
     */
    public boolean removeToken(HttpServletRequest request) {
        return removeToken(extractToken(request));
    }

    /**
     * 移除Token
     *
     * @param token token
     * @return boolean
     */
    public boolean removeToken(String token) {
        if (!tokenProperties.isStateful()) {
            log.debug("Token is not stateful, no need to remove");
            return true;
        }

        try {
            Map<String, Object> payload = tokenProvider.parseToken(token);
            Object id = parseId(payload);
            if (ObjectUtil.isEmpty(id)) {
                return false;
            }

            String clientId = parseClientId(payload);

            getTokenCacheService().remove(id, clientId, TokenType.ACCESS, token);

            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 解析Token
     *
     * @param token token
     * @return 载荷
     * @throws AuthenticationException 解析失败时抛出，如：Token过期、Token无效等
     */
    private Map<String, Object> parseToken(String token) throws AuthenticationException {
        Map<String, Object> payload = tokenProvider.parseToken(token);
        if (!tokenProperties.isStateful()) {
            return payload;
        }

        Object userId = parseId(payload);
        String clientId = parseClientId(payload);
        TokenType tokenType = parseType(payload);
        if (userId == null || clientId == null) {
            throw new AuthenticationException(SecurityErrorCode.INVALID_TOKEN);
        }

        String tokenInCache = getTokenCacheService().get(userId, clientId, tokenType, token);

        if (!Objects.equals(token, tokenInCache)) {
            throw new AuthenticationException(SecurityErrorCode.TOKEN_EXPIRED);
        }

        return payload;
    }

    /**
     * 检查标识符
     *
     * @param id 标识符
     */
    private void checkId(Object id) {
        if (id == null) {
            throw new IllegalArgumentException("Token id must not be null");
        }

        if (!(id instanceof String) && !(id instanceof Integer) && !(id instanceof Long)) {
            throw new IllegalArgumentException("Token id must be String or Integer or Long");
        }
    }

    /**
     * 检查客户端标识符
     *
     * @param clientId 标识符
     */
    private void checkClientId(String clientId) {
        if (clientId == null) {
            throw new IllegalArgumentException("Client id must not be null");
        }
    }

    /**
     * 组装Token载荷
     *
     * @param id       标识符
     * @param clientId 客户端ID
     * @param type     类型
     * @return
     */
    private Map<String, Object> assembleTokenPayload(Object id, String clientId, TokenType type) {
        return Kv.create()
            .set(PAYLOAD_KEY_ID, id)
            .set(PAYLOAD_KEY_CLIENT, clientId)
            .set(PAYLOAD_KEY_TYPE, type.getCode());
    }

    /**
     * 解析标识符
     *
     * @param payload 载荷
     * @return 标识符
     */
    @SuppressWarnings("unchecked")
    private <T> T parseId(Map<String, ?> payload) {
        return (T) payload.get(PAYLOAD_KEY_ID);
    }

    /**
     * 解析客户端标识符
     *
     * @param payload 载荷
     * @return 客户端标识符
     */
    private String parseClientId(Map<String, ?> payload) {
        return (String) payload.get(PAYLOAD_KEY_CLIENT);
    }

    /**
     * 解析Token类型
     *
     * @param payload 载荷
     * @return Token类型
     * @throws AuthenticationException 解析失败时抛出，如：Token无效、Token过期等
     */
    private TokenType parseType(Map<String, ?> payload) {
        return TokenType.of((String) payload.get(PAYLOAD_KEY_TYPE));
    }

    /**
     * 检查Token类型
     *
     * @param payload token
     * @param type    类型
     */
    private void checkTokenType(Map<String, ?> payload, TokenType type) throws AuthenticationException {
        if (!Objects.equals(parseType(payload), type)) {
            throw new AuthenticationException(SecurityErrorCode.INVALID_TOKEN_TYPE);
        }
    }

    /**
     * 提取当前请求中的Token
     *
     * @param request 请求
     * @return Token
     */
    private String extractToken(HttpServletRequest request) {
        if (request == null) {
            return null;
        }

        // 获取头信息中的Token
        String token = request.getHeader(tokenProperties.getHeaderKey());
        if (StringUtil.isBlank(token)) {
            // 获取参数中的Token
            token = URLUtil.decode(request.getParameter(tokenProperties.getParamKey()));
            if (StringUtil.isBlank(token)) {
                return null;
            }
        }

        // 匹配Bearer
        Matcher matcher = TokenConstant.BEARER_PATTERN.matcher(token);

        return matcher.find() ? matcher.group(1) : token;
    }

    /**
     * 获取TokenCacheService
     *
     * @return TokenCacheService
     */
    private TokenCacheService getTokenCacheService() {
        if (tokenCacheService == null) {
            throw new IllegalStateException("TokenCacheService is not configured, do not use stateful-related features with a non-stateful token");
        }
        return tokenCacheService;
    }

}
